第 2 课:自动求导
PyTorch 的自动求导功能是通过 autograd 模块实现的,它支持基于计算图的反向传播机制。
1. 自动求导的核心概念
-
计算图(Computational Graph)
- 对某个张量的操作会构成一个有向无环图(DAG),其中:
- 节点表示张量。
- 边表示操作(如加法、乘法等)。
- 图中的信息用于记录计算路径,反向传播时按照图的依赖关系计算梯度。
- 对某个张量的操作会构成一个有向无环图(DAG),其中:
-
张量的
requires_grad属性- 如果张量的
requires_grad属性被设置为 True,则会记录对该张量的计算过程。 - 对这些张量执行的操作会被追踪,构成计算图。
- 如果张量的
-
反向传播
- 调用
backward()方法时,PyTorch 会自动沿着计算图执行反向传播,计算每个张量的梯度。
- 调用
-
梯度存储
- 梯度存储在张量的
.grad属性中。 - 默认情况下,梯度会累积在
.grad中 ,需手动清零避免干扰后续计算。
- 梯度存储在张量的
2. 对标量计算梯度
# 一层
import torch
x = torch.tensor([1.0], requires_grad=True) # 初始化一个需要求导的张量
y = x**2 + 3 * x + 5
y.backward() # 执行反向传播
print("x: ", x) # 打印 x 的值
print("dy/dx:", x.grad) # 打印 x 的梯度
print()
# 两层
x = torch.tensor([1.0], requires_grad=True) # 初始化一个需要求导的张量
y = x**2 + 3 * x + 5
z = y*3
z.backward() # 执行反向传播
print("x: ", x) # 打印 x 的值
print("dz/dx:", x.grad) # 打印 x 的梯度
# 注意:数值类型只能是 float,不可以是 int!
x: tensor([1.], requires_grad=True)
dy/dx: tensor([5.])
x: tensor([1.], requires_grad=True)
dz/dx: tensor([15.])
3. 对非标量计算梯度
当 y 是一个非标量时,y.backward() 无法直接使用。
我们需要指定一个与 y 形状相同的权重张量(grad_outputs),指定每个元素在反向传播中的权重
对于一个非标量张量 y 和张量 x ,梯度的定义是:
即一个 雅可比矩阵,其每一项是:
import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2
grad_outputs = torch.tensor([1.0, 2.0]) # 指定 grad_outputs 权重
y.backward(grad_outputs)
print("y: ",y)
print("x: ", x)
print("dy/dx:", x.grad)
y: tensor([4., 9.], grad_fn=<PowBackward0>)
x: tensor([2., 3.], requires_grad=True)
dy/dx: tensor([ 4., 12.])
上面代码的数学解释如下:
权重:
最终梯度:
拓展到两层,求梯度方法如下
import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2
z = 3*y
grad_outputs = torch.ones(z.shape) # 自动获取 z 的尺寸
z.backward(grad_outputs)
print("x: ", x)
print("y: ", y)
print("z: ", z)
print("dy/dx:", x.grad)
x: tensor([2., 3.], requires_grad=True)
y: tensor([4., 9.], grad_fn=<PowBackward0>)
z: tensor([12., 27.], grad_fn=<MulBackward0>)
dy/dx: tensor([12., 18.])
如果 最终输出 是标量(如通过 .sum() 或 .mean() 将非标量张量变为标量),则不需要显式指定 grad_outputs。
import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2 # y 是非标量
z = y.sum() # z 是标量
z.backward() # 不需要 grad_outputs
print("x.grad:", x.grad)
x.grad: tensor([4., 6.])
上面代码的数学解释如下:
如果你不想累积梯度到 .grad 属性,可以使用 torch.autograd.grad() 方法,直接返回梯度值。
import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x**2
grad_outputs = torch.tensor([1.0, 1.0]) # 对应 y 的权重
grad = torch.autograd.grad(y, x, grad_outputs=grad_outputs) #不会修改张量的 .grad 属性,而是直接返回一个元组
print("Gradient:", grad)
print("x.grad:",x.grad)
Gradient: (tensor([4., 6.]),)
x.grad: None
如果需要完整的雅可比矩阵,可以使用 torch.autograd.functional.jacobian。
import torch
x = torch.tensor([2.0, 3.0], requires_grad=True)
def func(x): #定义计算过程
return torch.tensor([x[0]**2, x[1]**3])
jacobian = torch.autograd.functional.jacobian(func, x)
print("Jacobian matrix:")
print(jacobian)
Jacobian matrix:
tensor([[0., 0.],
[0., 0.]])
上面代码的数学原理如下:
对于 ,
补充知识点:
在梯度反向传播结束后, 只有叶子节点的梯度得到保留,非叶子结点的梯度会被释放掉
如果需要保留的话可以对该结点设置retain_grad()
4. 阻止梯度追踪
在某些情况下,你可能不希望某些张量参与梯度计算,可以使用以下方法:
方法一:禁言计算图构建
import torch
x = torch.tensor([2.0], requires_grad=True)
with torch.no_grad(): # 在这个板块下的计算不会被计算图追踪
y = x**2
print("y.requires_grad:", y.requires_grad)
y.backward
print("x.grad:",x.grad)
print("x.grad:",x.grad)
y.requires_grad: False
x.grad: None
x.grad: None
方法二:从计算图中分离张量。
import torch
x = torch.tensor([2.0], requires_grad=True)
y = x**2
z = y.detach() + x*3
z.backward()
print("z:",z)
print("x.grad:", x.grad) # x 的梯度正常计算
z: tensor([10.], grad_fn=<AddBackward0>)
x.grad: tensor([3.])
在输出中,我们可以看到 z 的值是正常的,但是在求导过程中,到 y 时就会把y 当成一个常数,而不会沿 y 继续求导
也就是 y.detach() 会切断 y 的计算图,y 的梯度不会被追踪
注意:y.detach() 的返回值 和 y 共享数据存储,修改一个会影响另一个。
5. 梯度清零
默认情况下,PyTorch 会累积梯度。因此在每次反向传播前,需要手动清零梯度:
x = torch.tensor([2.0], requires_grad=True)
y1 = x**2
y2 = x**2
y1.backward()
print("dy1/dx:", x.grad)
y2.backward()
print("dy2/dx:", x.grad)
x.grad.zero_() # 清零之前的梯度
print("x.grad:",x.grad)
dy1/dx: tensor([4.])
dy2/dx: tensor([8.])
x.grad: tensor([0.])
6. 自定义梯度计算方式
在 PyTorch 中,通过 def 定义的计算步骤 也可以进行自动求导的。
单输入函数
import torch
x = torch.tensor([2.0], requires_grad=True)
def my_function(x): # 定义函数
return x**2 + 3*x + 5
y = my_function(x)
y.backward()
print("x:", x)
print("y:",y)
print("dy/dx:", x.grad)
x: tensor([2.], requires_grad=True)
y: tensor([15.], grad_fn=<AddBackward0>)
dy/dx: tensor([7.])
多输入函数
x = torch.tensor([2.0], requires_grad=True)
y = torch.tensor([3.0], requires_grad=True)
def my_function(x, y):
return x**2 + y**3
z = my_function(x, y)
z.backward()
print("z:",z)
print("dz/dx:", x.grad)
print("dz/dy:", y.grad)
z: tensor([31.], grad_fn=<AddBackward0>)
dz/dx: tensor([4.])
dz/dy: tensor([27.])
多输出函数
x = torch.tensor([2.0], requires_grad=True)
def my_function(x):
return x**2, x**3
y1, y2 = my_function(x)
y1.backward(retain_graph=True) # 保留计算图以支持后续反向传播
print("dy1/dx:", x.grad)
x.grad.zero_() # 清零之前的梯度
y2.backward()
print("dy2/dx:", x.grad)
dy1/dx: tensor([4.])
dy2/dx: tensor([12.])
包含复杂逻辑的函数
x = torch.tensor([2.0], requires_grad=True)
def my_function(x):
if x > 0:
return x**2
else:
return x**3
y = my_function(x)
y.backward()
print("x:", x)
print("dy/dx:", x.grad)
x: tensor([2.], requires_grad=True)
dy/dx: tensor([4.])
此外还可以明确指定梯度的反向传播步骤。
这是通过继承 torch.autograd.Function 并实现其 forward 和 backward 静态方法完成的。
下面展示了一个例子,是对自定义函数
import torch
class SquareFunction(torch.autograd.Function): #继承 torch.autograd.Function
@staticmethod
def forward(ctx, input): # 负责前向计算。
# `ctx` 是一个上下文对象,用于在 `forward` 和 `backward` 中共享数据,如输入值、中间结果
ctx.save_for_backward(input) # 保存输入张量,以在反向传播中使用
# 也可以直接通过 `ctx` 保存信息,例如: ctx.constant = 2
return input ** 2 # 返回结果(通常是张量)
@staticmethod
def backward(ctx, grad_output): #输入的 grad_output 是上一步的梯度
input, = ctx.saved_tensors # 从 ctx 中恢复保存的输入值
grad_input = 2 * input * grad_output # 计算梯度。这一步是 x^2 的求导 2x。并使用了链式法则
return grad_input # 返回结果需要与 `forward` 的输入数量和形状一致,对应每个输入的梯度。
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = SquareFunction.apply(x) # 使用 apply 调用自定义函数
z = y.sum()
z.backward() # 对标量调用 backward
print("x:",x)
print("y:",y)
print("z:",z)
print("x.grad:", x.grad) # 打印梯度
x: tensor([2., 3.], requires_grad=True)
y: tensor([4., 9.], grad_fn=<SquareFunctionBackward>)
z: tensor(13., grad_fn=<SumBackward0>)
x.grad: tensor([4., 6.])
多输入多输出函数如:
import torch
class MultiInputOutputFunction(torch.autograd.Function):
@staticmethod
def forward(ctx, x0, x1):
ctx.save_for_backward(x0, x1) # 保存输入值
return x0 + x1, x0 - x1 # 返回两个结果
@staticmethod
def backward(ctx, grad_output1, grad_output2):
x0, x1 = ctx.saved_tensors # 从 ctx 恢复输入
grad_x0 = grad_output1 + grad_output2 # 计算每个输入的梯度
grad_x1 = grad_output1 - grad_output2
return grad_x0, grad_x1
x0 = torch.tensor(2.0, requires_grad=True)
x1 = torch.tensor(3.0, requires_grad=True)
y1, y2 = MultiInputOutputFunction.apply(x0, x1)
z = y1 + y2 # 定义标量函数
z.backward()
print("x0.grad:", x0.grad) # 梯度: grad_x0
print("x1.grad:", x1.grad) # 梯度: grad_x1
x0.grad: tensor(2.)
x1.grad: tensor(0.)
上面代码的数学解释:
- 前向计算:
- 反向传播:
复杂的自定义操作,例如
class CubeFunction(torch.autograd.Function):
@staticmethod
def forward(ctx, input):
ctx.save_for_backward(input)
return input ** 3 + 5
@staticmethod
def backward(ctx, grad_output):
input, = ctx.saved_tensors
grad_input = 3 * input ** 2 * grad_output
return grad_input
x = torch.tensor([2.0], requires_grad=True)
y = CubeFunction.apply(x)
y.backward()
print("x.grad:", x.grad) # 打印梯度
7. 静态图和动态图
计算图根据计算图的搭建方式可以划分为静态图和动态图。
pytorch是典型的动态图,TensorFlow是静态图(TF 2.x 也支持动态图模式)。
区分动态图和静态图:
-
第一种判断:运算是在计算图搭建之后,还是两者同步进行
先搭建计算图,再运算,这就是静态图机制。
而在运算的同时去搭建计算图,这就是动态图机制。 -
第二种判断:运算过程中,计算图是否可变动
在运算过程中,计算图可变动的是动态图;
计算图不可变,是静止的,就是静态图。